package org.jeecg.modules.security.util;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.jeecg.common.util.OSUtil;
import org.jeecg.modules.security.entity.HTUploadFileValidateResult;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


@Slf4j
public class HTUploadFileValidator {
    private static final String[] MimeTypeWhiteList = {
            "image/png",
            "image/jpeg",
            "image/jpg",
            "video/mpeg",
            "video/mp4",
            "application/vnd.ms-excel",
            "application/msword",//doc
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",//docx,
            "application/pdf",//pdf
            "application/vnd.android.package-archive"
    };

    private static final String[] SuffixList = {
            ".png",
            ".jpeg",
            ".jpg",
            ".xlsx",
            ".xls",
            ".mp4",
            ".doc",
            ".docx",
            ".pdf",
            ".apk"
    };

    //    ZIP Archive (zip)，文件头：504B0304
//    MS Word/Excel (xlsx pptx docx)，文件头: 504B0304
//    当 docx 没有内容时，没有找到文件头；当它有内容时，它有文件头：504B0304
//  https://blog.mythsman.com/post/5d301940976abc05b345469f/
//  https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
//  https://en.wikipedia.org/wiki/List_of_file_signatures
// 文件头：https://zhuanlan.zhihu.com/p/571208394
//    ffd8ffdb
    private static final String[] HexHeaderList = {
            "ffd8ffdb", //jpeg(jpg)////https://en.wikipedia.org/wiki/List_of_file_signatures
            "ffd8ffee", //jpeg(jpg)////https://en.wikipedia.org/wiki/List_of_file_signatures
            "ffd8ffe0", //jpeg(jpg)//https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E5%A4%B4/2695144
            "ffd8ffe1", //jpeg(jpg)//https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E5%A4%B4/2695144
            "ffd8ffe8", //jpeg(jpg)//https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E5%A4%B4/2695144
            "52494646",//jpg RIFF file format https://www.cnblogs.com/wangguchangqing/p/5957531.html
            "89504e47", //png
            "d0cf11e0", //xls.or.doc
            "504b0708", //xlsx
            "00000018", //mp4 MPEG-4 video files
            "00000020",//m4a; m4v MPEG-4 video files
            "000001ba", //MPEG
            "0000001c",//mp4
            "000001b3", //mpg; mpeg MPEG Movie
            "504b0304", //zip xlsx pptx docx //(对照表的英文大写要改成小写，第一个数字为0时要省略，eg:504B0304读到的是504b34)
            "25504446", //pdf
    };

    static public HTUploadFileValidateResult validateUploadFileByRequest(HttpServletRequest request) {
        HTUploadFileValidateResult result = new HTUploadFileValidateResult();
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        MultipartFile file = multipartRequest.getFile("file");

        // 获得文件到map容器中, 判断FileMap是否为空
        Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
//        if (Objects.isNull(fileMap) || fileMap.size() < 0) {
//
//            return
//        }
        result.setPassed(fileMap.size() > 0);

        //0 验证Request Content-Type 是否为multipart/form-data 判断请求是否为multipart请求
        result.setPassed(ServletFileUpload.isMultipartContent(multipartRequest));

        // 1 验证File Content-Type
        result.setPassed(checkFileMimeType(file));

//        boolean isValidFileExt = fileCheckService.checkFileExt(fileFullName);
//        if(!isValidFileExt) {
//            messages.append("文件[").append(fileFullName).append("]上传失败，原因：文件格式不允许！");
//            continue;
//        }


        //2 验证后缀名
        result.setPassed(checkFileSuffix(file));

        //3 验证文件名
        result.setPassed(checkFileName(file));

        //4 图片校验，防止图片木马
        result.setPassed(checkFileHexHeader(file));

        //5 判断上传文件大小
        result.setPassed(checkFileSize(file));


        return null;
    }

    static public boolean checkFileMimeType(MultipartFile file) {
        String contentType = file.getContentType();//获取请求的content-type
        System.out.printf("\n\n\n MimeType %s \n\n\n", JSONObject.toJSONString(contentType));
//        https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
        long count = Arrays.asList(MimeTypeWhiteList).stream().filter(ext -> ext.equals(contentType)).count();
        //如果结果大于0，即存在于白名单中

        return count > 0;
    }

    static public boolean checkFileName(MultipartFile file) {
        String[] specialCharacterList = {"::$data", "::$index_allocation", ";", "%00", "0x00", "/."};
        String fileName = file.getOriginalFilename().toLowerCase();
        System.out.printf("\n\n\n %s \n\n\n", JSONObject.toJSONString(fileName));
        assert fileName != null;
        boolean contains = false;
        for (String character : specialCharacterList) {
            if (fileName.contains(character)) {
                contains = true;
                break;
            }
        }

        System.out.printf("\n\n\n 包含特殊字符串 contains = %s \n\n\n", JSONObject.toJSONString(contains));
        return !contains;
    }

    static public boolean checkFileSuffix(MultipartFile file) {
        String filename = file.getOriginalFilename();
        assert filename != null;
        String suffix = filename.substring(filename.lastIndexOf('.')).toLowerCase();
        log.info("\n\ncheckFileSuffix suffix : {}\n\n", suffix );
        long count = Arrays.stream(SuffixList).filter(ext -> ext.equals(suffix)).count();
        //如果结果大于0，即存在于白名单中

        return count > 0;
    }

    /**
     * 文件头校验
     *
     * @file MultipartFile 文件
     * @Description 魔码数字：https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
     * https://cloud.tencent.com/developer/article/1480182?from=article.detail.1747555
     */
    //zip-504b34
    static public boolean checkFileHexHeader(MultipartFile file) {
        try {
            InputStream inputStream = file.getInputStream();
            int len = -1;
            StringBuilder builder = new StringBuilder(4);
            String subHexString;
            for (int i = 0; i < 4 && -1 != (len = inputStream.read()); i++) {
                // 以十六进制（基数 16）无符号整数形式返回一个整数参数的字符串表示形式
                subHexString = Integer.toHexString(len & 0xFF);
                // toHexString API 当第一个数字为0时要省略，eg:504B0304读到的是504b34，因此当subHexString length小于2时要在高位拼接0
                if (subHexString.length() < 2) {
                    builder.append(0);
                }
                builder.append(subHexString);
            }
            String hexHeader = builder.toString().toLowerCase();
            log.info("\n\n checkFileHexHeader hexHeader : {}\n\n", hexHeader );
            long count = Arrays.stream(HexHeaderList).filter(ext -> ext.equals(hexHeader)).count();
            //如果结果大于0，即存在于白名单中

            return count > 0;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }


    /**
     * 通过读取文件并获取其width及height的方式，来判断判断当前文件是否图片，这是一种非常简单的方式。
     *
     * @param imageFile
     * @return
     */
    public static boolean isImage(File imageFile) {
        if (!imageFile.exists()) {
            return false;
        }
        Image img = null;
        try {
            img = ImageIO.read(imageFile);
            if (img == null || img.getWidth(null) <= 0 || img.getHeight(null) <= 0) {
                return false;
            }
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            img = null;
        }
    }

    static public boolean checkFileSize(MultipartFile file) {
        long size = file.getSize();

        String filename = file.getOriginalFilename();
        String suffix = filename.substring(filename.lastIndexOf('.')).toLowerCase();
        if (suffix.equals(".mp4")) {
            return size <= 300 * 1024 * 1024;
        } else if (suffix.equals(".pdf")) {
            return size <= 300 * 1024 * 1024;
        } else if (suffix.equals(".doc")) {
            return size <= 300 * 1024 * 1024;
        } else if (suffix.equals(".docx")) {
            return size <= 300 * 1024 * 1024;
        } else {
            return size <= 10 * 1024 * 1024;
        }
    }

    public static String strSpecialFilter(String str) {
        String regEx = "[\\u00A0\\s\"`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Pattern p = Pattern.compile(regEx);
        Matcher m = p.matcher(str);
        //将所有的特殊字符替换为空字符串
        return m.replaceAll("").trim();
    }

    // https://www.cnblogs.com/firstdream/p/7373769.html
    // https://www.cxyzjd.com/article/qq_39706128/84582846
    public static boolean disabledFileOrFolderExecutable(String folderOrFilePath) {
        File dirFile = new File(folderOrFilePath);
        System.out.printf("\n\n\n before disabledFileOrFolderExecutable = %s canExecute %s \n\n\n", folderOrFilePath, dirFile.canExecute());
        if (dirFile.canExecute()) {
            updateLinuxOrDigitalUnixPermissionIfNeeded("777", folderOrFilePath, dirFile);
            dirFile.setExecutable(false, false);
            updateLinuxOrDigitalUnixPermissionIfNeeded("644", folderOrFilePath, dirFile);
            System.out.printf("\n\n\n After disabledFileOrFolderExecutable = %s canExecute %s \n\n\n", folderOrFilePath, dirFile.canExecute());
        }


        return !dirFile.canExecute();
    }

    public static boolean updateLinuxOrDigitalUnixPermissionIfNeeded(String permission, String folderOrFilePath, File dirFile) {
        boolean succeed = false;
        if (OSUtil.isLinux() || OSUtil.isMacOSX() || OSUtil.isDigitalUnix()) {
            Runtime runtime = Runtime.getRuntime();
            String command = String.format("chmod %s %s", permission, folderOrFilePath);
            System.out.printf("\n\n\n command %s \n\n\n", JSONObject.toJSONString(command));
            try {
                Process process = runtime.exec(command);
                process.waitFor();
                int existValue = process.exitValue();
                succeed = (existValue == 0);
                System.out.printf("\n\n\n updateLinuxOrDigitalUnixPermissionIfNeeded Change file %s permission  existValue = %s. \n\n\n", permission, existValue);
            } catch (Exception e) {
                System.out.printf("\n\n\n Exception Change file %s  permission failed %s \n\n\n", permission);
                return false;
            }
        }

        return succeed;
    }


    public static boolean getFileOrFolderCanExecutable(String path) {
        File dirFile = new File(path);
        return dirFile.canExecute();
    }


    //防御的核心在于对传入的文件名进行正确的过滤，防止跨目录。
//    private boolean checkFilename(String filename) {
//        filename = filename.replace("\\", "/"); //消除平台差异
//        //将文件操作限定在指定目录中
//        File file = new File(filename);
//        if(file == null || !file.exists() || file.getCanonicalFile().getParent().equals(new File(DOWNLOAD_PATH).getCanonicalPath())) { //检测上级目录是否为指定目录
//            return false;
//        }
//        //检测文件名中是否有敏感字符
//        List<String> blackList = Arrays.asList("./", "../");
//        for(String badChar : blackList) {
//            if(filename.contains(badChar)) {
//                return false;
//            }
//        }
//        //对文件后缀名设置白名单
//        List<String> whiteExt = Arrays.asList("png", "jpg", "gif", "jpeg", "doc");
//        String fileExt = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
//        if(!whiteExt.contains(fileExt)) {
//            return false;
//        }
//        return true;
//    }

    // 文件写入与上述三个操作略有不同，在防御时需要对写入的内容进行过滤。
//    private boolean checkFilecontent(String fileContent) {
//        List<String> blackContents = Arrays.asList("<%@page", "<%!");
//        for(String blackContent : blackContents) {
//            if(fileContent.contains(blackContent)) {
//                return false;
//            }
//        }
//        return true;
//    }
    //在写入时最好将标签进行htmlencode
//    String fileContent = request.getParameter("content");
//    fileContent = ESAPI.encoder().encodeForHTML(fileContent);

//    文件头16进制
//    JPEG (jpg)，文件头：FFD8FF
//    PNG (png)，文件头：89504E47
//    GIF (gif)，文件头：47494638
//    TIFF (tif)，文件头：49492A00
//    Windows Bitmap (bmp)，文件头：424D
//    CAD(dwg)，文件头：41433130
//    Adobe Photoshop (psd)，文件头：38425053
//    Rich Text Format (rtf)，文件头：7B5C727466
//    XML (xml)，文件头：3C3F786D6C
//    HTML(html)，文件头：68746D6C3E
//    Email [thorough only] (eml)，文件头:44656C69766572792D646174653A
//    Outlook Express(dbx)，文件头：CFAD12FEC5FD746F
//    Outlook (pst)，文件头：2142444E
//    MS Word/Excel(xls.or.doc)，文件头：D0CF11E0
//    MS Access (mdb)，文件头：5374616E64617264204A
//    WordPerfect (wpd)，文件头：FF575043
//    Adobe Acrobat (pdf)，文件头：255044462D312E
//    Quicken (qdf)，文件头：AC9EBD8F
//    Windows Password (pwl)，文件头：E3828596
//    ZIP Archive (zip)，文件头：504B0304
//    RAR Archive (rar)，文件头：52617221
//    Wave(wav)，文件头：57415645
//    AVI (avi)，文件头：41564920
//    Real Audio(ram)，文件头：2E7261FD
//    Real Media (rm)，文件头：2E524D46
//    MPEG(mpg)，文件头：000001BA
//    MPEG (mpg)，文件头：000001B3
//    Quicktime(mov)，文件头：6D6F6F76
//    Windows Media (asf)，文件头：3026B2758E66CF11
//    MIDI(mid)，文件头：4D546864


//     使用fileUpload组件实现文件上传除了上面的那些方法以外还要注意的：
//    文件名中文乱码处理：servletFileUpload.setHeaderEncoding("utf-8") 或 request.setCharacterEncoding("utf-8");
//    表单普通字段中文乱码处理：new String(str.getBytes("iso-8859-1","utf-8"));
//    设置内存缓冲区的大小，默认为10KB：diskFileItemFactory.setSizeThreshold(1024*1024);
//    指定临时文件目录，若是单个文件的大小超过内存缓冲区，该文件将会临时缓存在此目录下：diskFileItemFactory.setRepository(file);
//    设置单个文件大小限制，若是有某个文件超过此大小，将抛出FileUploadBase.FileSizeLimitExceededException：servletFileUpload.setFileSizeMax(1024*1024*10);
//    设置全部文件，也就是请求大小限制，若是文件总和超过此大小，将抛出FileUploadBase.SizeLimitExceededException：servletFileUpload.setSizeMax(1024*1024*20);
//    利用UUID生成伪随机字符串做为文件名避免重复：UUID.randomUUID().toString();
//    将文件写到硬盘上。写完以后，系统会自动将放在临时文件目录的该文件删除：fileItem.write(new File(path,fileName));
//    注：若是没有指定临时文件目录，默认采用系统默认的临时文件路径，能够经过System.getProperty("java.io.tmpdir")获取，
//    Tomcat系统默认临时目录为“<tomcat安装目录>/temp/”。
//    Apache文件上传组件在解析上传数据中的每一个字段内容时，须要临时保存解析出的数据，以便在后面进行数据的进一步处理
//    （保存在磁盘特定位置或插入数据库）。由于Java虚拟机默承认以使用的内存空间是有限的，超出限制时将会抛出“java.lang.OutOfMemoryError”错误。
//    若是上传的文件很大，例如800M的文件，在内存中将没法临时保存该文件内容，Apache文件上传组件转而采用临时文件来保存这些数据；
//    但若是上传的文件很小,例如600个字节的文件,显然将其直接保存在内存中性能会更加好些。
//    文件上传细节
//    如果上传文件的大小大于我们设定文件的大小，那么文件在上传的时候会使用临时文件保存上传数据。在上传完毕后，我们应该删除临时文件
//    上传文件的位置是不能在WEB服务器管理之下的，否则可能造成安全问题【其他人有可能通过手段来修改上传文件】
//    如果上传文件名相同，那么就会把原本的上传文件覆盖掉。我们要生成一个独一无二的文件名。
//    如果用户量很大，上传文件非常多。那么我们不应该在一个目录保存所有的上传文件，这样很可能造成磁盘奔溃了。所以我们要把上传的文件打散到不同的目录下。


}
